package hz;

import java.io.BufferedReader;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import org.codehaus.jackson.map.ObjectMapper;

/**
 * Communicator.
 * 
 * @author Hechuan
 * 
 */
public class Communicator implements Closeable {

	private static final ObjectMapper mapper = new ObjectMapper();

	private String hostName = "127.0.0.1";
	private int portNumber = 60001;

	private Socket socket;
	private PrintWriter out;
	private BufferedReader in;

	// need to reset after every round
	private int roomid = -1;
	private int rivalId = -1;

	private Map<String, Object> getMap() {
		return new HashMap<String, Object>();
	}

	/**
	 * Constructor. Socket will be initialized.
	 */
	public Communicator() {
		try {
			socket = new Socket(hostName, portNumber);
			out = new PrintWriter(socket.getOutputStream(), true);
			in = new BufferedReader(new InputStreamReader(
					socket.getInputStream()));
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Say something to server.
	 * 
	 * @param map
	 * @return
	 */
	@SuppressWarnings("unchecked")
	private Map<String, Object> say(Map<String, Object> map) {
		try {
			out.println(mapper.writeValueAsString(map));
			String fromServer;
			while ((fromServer = in.readLine()) != null) {
				return (Map<String, Object>) mapper.readValue(fromServer,
						Map.class);
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
		return Collections.emptyMap();
	}

	/**
	 * Do remember close it after you are done with it.
	 */
	@Override
	public void close() {

		try {

			if (socket != null)
				socket.close();
			if (out != null)
				out.close();
			if (in != null)
				in.close();
		} catch (IOException e) {
			e.printStackTrace();
		}

	}

	/**
	 * Sign up.
	 * 
	 * @param userid
	 * @param passwd
	 * @return the new assigned id. and 0 indicates an error.
	 */
	public int signUp(String userid, String passwd) {
		Map<String, Object> map = getMap();
		map.put("action", "newuser");
		map.put("userid", userid);
		map.put("passwd", passwd);

		Map<String, Object> ret = say(map);

		if ("oops".equals(ret.get("status")))
			return 0;
		return (int) ret.get("id");
	}

	/**
	 * Sign in.
	 * 
	 * @param userid
	 * @param passwd
	 * @return user id. 0 indicates an error.
	 */
	public int signIn(String userid, String passwd) {
		Map<String, Object> map = getMap();
		map.put("action", "signin");
		map.put("userid", userid);
		map.put("passwd", passwd);

		Map<String, Object> ret = say(map);

		return "oops".equals(ret.get("status")) ? 0 : (int) ret.get("id");
	}

	/**
	 * List available rooms.
	 * 
	 * @return
	 */
	public Room[] lsRooms() {
		Map<String, Object> map = getMap();
		map.put("action", "lsroom");

		Map<String, Object> ret = say(map);
		if ("oops".equals(ret.get("status")))
			return new Room[0];
		return mapper.convertValue(ret.get("list"), mapper.getTypeFactory()
				.constructArrayType(Room.class));
	}

	/**
	 * Create a game room.
	 * 
	 * @param uid
	 * @param roomname
	 * @param passwd
	 * @return instance of Room. Do remember to validate room id before you use
	 *         it. an id of 0 indicates an error.
	 */
	public Room createRoom(int uid, String roomname, String passwd) {
		Map<String, Object> map = getMap();
		map.put("action", "createroom");
		map.put("id", uid);
		map.put("roomname", roomname);
		if (passwd != null)
			map.put("passwdforroom", passwd);

		Map<String, Object> ret = say(map);

		if ("oops".equals(ret.get("status")))
			return new Room();
		Room room = mapper.convertValue(ret.get("room"), Room.class);
		roomid = room.getId();
		return room;
	}

	/**
	 * Create a public game room.
	 * 
	 * @param uid
	 * @param roomname
	 * @see Communicator#createRoom(int, String, String)
	 * @return
	 */
	public Room createRoom(int uid, String roomname) {
		return createRoom(uid, roomname, null);
	}

	/**
	 * Join a public room.
	 * 
	 * @see #joinRoom(Room, String)
	 * @param room
	 * @return
	 */
	public boolean joinRoom(Room room) {
		return joinRoom(room, null);
	}

	/**
	 * Join a room
	 * 
	 * @param room
	 * @param password
	 * @return success or failure
	 */
	public boolean joinRoom(Room room, String password) {
		Map<String, Object> map = getMap();
		map.put("action", "join");
		map.put("roomid", room.getId());
		if (password != null)
			map.put("passwd", password);

		Map<String, Object> ret = say(map);

		if (!"oops".equals(ret.get("status"))) {
			this.roomid = room.getId();
			rivalId = room.getAid();
			return true;
		}
		return false;
	}

	/**
	 * Wait for your rival.
	 * 
	 * @return id of your rival. 0 indicates an error.
	 */
	@SuppressWarnings("unchecked")
	public int waitForRival() {
		try {
			String fromServer = in.readLine();
			Map<String, Object> ret = mapper.readValue(fromServer, Map.class);
			rivalId = (int) ret.get("bid");
			return (int) ret.get("bid");

		} catch (IOException e) {
		}
		return 0;
	}

	/**
	 * Do I have to explain this?
	 */
	public void abandon() {
		Map<String, Object> map = getMap();
		map.put("action", "abandon");
		map.put("to", rivalId);
		map.put("roomid", roomid);

		say(map);
	}

	/**
	 * Make a movement.
	 * 
	 * @param row
	 * @param col
	 * @return youwin/youlose/draw/ok/oops, meaning you win, you lose, draw,
	 *         move successfully and failed to move (not your turn, overlap,
	 *         communication issue, sort of), respectively.
	 */
	public String move(int row, int col) {

		if (roomid <= 0 || rivalId <= 0)
			return "";

		Map<String, Object> map = getMap();
		map.put("action", "move");
		map.put("row", row);
		map.put("col", col);
		map.put("to", rivalId);

		map.put("roomid", roomid);

		return (String) say(map).get("status");
	}

	/**
	 * WaitForMove
	 * 
	 * @param pos
	 *            , the 1st element will be set to row, 2nd to column
	 * @see #move(int, int)
	 * @return refer to {@link #move(int, int)}
	 */
	@SuppressWarnings("unchecked")
	public String waitForMove(int[] pos) {
		if (pos == null || pos.length != 2)
			return "";
		try {
			String fromServer = in.readLine();
			Map<String, Object> ret = mapper.readValue(fromServer, Map.class);
			pos[0] = (int) ret.get("row");
			pos[1] = (int) ret.get("col");
			return (String) ret.get("status");
		} catch (IOException e) {
		}
		return "";
	}

	/**
	 * Request user info.
	 * 
	 * @param id
	 * @return
	 */
	public User getUser(int id) {
		Map<String, Object> map = getMap();
		map.put("action", "getuser");
		map.put("id", id);

		Map<String, Object> ret = say(map);

		if ("oops".equals(ret.get("status")))
			return new User();
		return mapper.convertValue(ret.get("user"), User.class);
	}

}
